﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using Heimdallr.Async;

namespace Heimdallr.Net
{ 
    /// <summary>
    /// Represents the notification mechanism base on HTTP multipart stream.
    /// </summary>
    /// <remarks> 
    /// For additional information about multipart, see RFC1341 - MIME (Multipurpose Internet Mail Extensions) section 7.2. 
    /// </remarks>
    public class HttpMultipartResponseModule : HttpModule, IHttpResponseModule
    {
        #region Field

        /// <summary>
        /// Represents the default value of <see cref="HttpModule.RelativePath"/> property.
        /// This field is read-only.
        /// </summary>
        public readonly static string DefaultRelativePath = "svc-bin/streaming/";

        /// <summary>
        /// Get the current boundary which <see cref="HttpMultipartResponseModule"/> instances are using.
        /// This field is read-only.
        /// </summary>
        public readonly static string Boundary = string.Concat("--", Guid.NewGuid().ToString("N").Substring(12));

        /// <summary>
        /// Represents the <see cref="HttpResponseHeader">HttpResponseHeader.ContentType</see> value.
        /// This field is read-only.
        /// </summary>
        public readonly static string ContentType = string.Concat("multipart/x-mixed-replace; boundary=", Boundary, ";");

        private readonly ResponseWrapperCollection<PublishMission> wrappers;
        private readonly object locker;
        private readonly Encoding encoding;
        private readonly byte[] boundaryCrlf;
        private readonly int boundaryCrlfLength;

        private Guid? scheduleId;
        private int duration;

        #endregion

        #region Property

        /// <summary>
        /// Gets a collection represents connected clients.
        /// </summary>
        public IEnumerable<HttpResponseContext> Responses
        {
            get { return this.wrappers.Select(r => r.Response); }
        }

        #endregion

        #region Constructor

        /// <summary>
        /// Initializes a new instance with HTTP <see cref="WebRequestMethods.Http.Get">GET</see> method and <see cref="DefaultRelativePath"/>.
        /// </summary>
        public HttpMultipartResponseModule()
            : this(WebRequestMethods.Http.Get, DefaultRelativePath, HttpModuleCredentialType.None)
        {
        }

        /// <summary>
        /// Initializes a new instance with HTTP <see cref="WebRequestMethods.Http.Get">GET</see> method and indicated path.
        /// </summary>
        /// <param name="relativePath">HTTP relative path.</param>
        /// <exception cref="ArgumentException">Parameter <paramref name="relativePath"/> must end with character '/'.</exception>
        public HttpMultipartResponseModule(string relativePath)
            : this(WebRequestMethods.Http.Get, relativePath, HttpModuleCredentialType.None)
        {
        }

        /// <summary>
        /// Initializes a new instance with HTTP <see cref="WebRequestMethods.Http.Get">GET</see> method, indicated path and credential type.
        /// </summary>
        /// <param name="relativePath">HTTP relative path.</param>
        /// <param name="credentialType">HTTP credential type.</param>
        /// <exception cref="ArgumentNullException">Parameter <paramref name="relativePath"/> is null.</exception>
        /// <exception cref="ArgumentException">Parameter <paramref name="relativePath"/> must end with character '/'.</exception>
        /// <exception cref="InvalidEnumArgumentException">Parameter <paramref name="credentialType"/> is not a valid value.</exception>
        public HttpMultipartResponseModule(string relativePath, HttpModuleCredentialType credentialType)
            : this(WebRequestMethods.Http.Get, relativePath, credentialType)
        { 
        }

        /// <summary>
        /// Initializes a new instance with indicated HTTP method, path and credential type.
        /// </summary>
        /// <param name="method">HTTP method.</param>
        /// <param name="relativePath">HTTP relative path.</param>
        /// <param name="credentialType">HTTP credential type.</param>
        /// <exception cref="ArgumentNullException">Parameter <paramref name="method"/> or <paramref name="relativePath"/> is null.</exception>
        /// <exception cref="ArgumentException">Parameter <paramref name="relativePath"/> must end with character '/'.</exception>
        /// <exception cref="InvalidEnumArgumentException">Parameter <paramref name="credentialType"/> is not a valid value.</exception>
        public HttpMultipartResponseModule(string method, string relativePath, HttpModuleCredentialType credentialType)
            : base(method, relativePath, credentialType)
        {
            this.wrappers = new ResponseWrapperCollection<PublishMission>();
            this.duration = 5;
            this.locker = new object();
            this.encoding = Encoding.Default;
            this.boundaryCrlf = this.encoding.GetBytes(string.Concat(Environment.NewLine, Boundary, Environment.NewLine));
            this.boundaryCrlfLength = this.boundaryCrlf.Length;
        }

        #endregion

        #region HttpModule Member

        public override bool Validate(string username, string password)
        {
            return true;
        }

        public override void Dispose()
        {
            foreach (var wrapper in this.wrappers)
            {
                wrapper.Response.Dispose();
                this.ContextUnregistered(wrapper.Response);
            }
        }

        #endregion

        #region IHttpResponseModule Member

        /// <summary>
        /// Registers an alive response previously which has prepared for notifying.
        /// </summary>
        /// <param name="response">An alive response created and passed by <see cref="HttpElement"/>.</param>
        /// <returns>If success, return true. Otherwise return false.</returns>
        /// <exception cref="ArgumentNullException"><paramref name="response"/> is null.</exception>
        bool IHttpResponseModule.RegisterContext(HttpResponseContext response)
        {
            if (response == null)
                throw new ArgumentNullException("response");

            try
            {
                response.Headers[HttpResponseHeader.ContentType] = ContentType;
                response.SendChunked = false;
                response.ContentLength = long.MaxValue;
                response.GetResponseStream().Write(this.boundaryCrlf, 0, this.boundaryCrlfLength);
            }
            catch (Exception error)
            {
                response.Dispose();
                Debug.WriteLine(error, this.GetType().FullName);

                return false;
            }

            lock (this.locker)
            {
                this.wrappers.Add(new ResponseWrapper<PublishMission>(response, this.MissionCallback));

                if (this.scheduleId == null)
                    this.scheduleId = SchedulePool.Register(this.CheckConnectionCallback, this.duration);
            }

            this.ContextRegistered(response);

            return true;
        }

        /// <summary>
        /// Publishes a notification to all registered <see cref="HttpResponseContext"/> instances.
        /// </summary>
        /// <param name="notification">A notification which is going to be published.</param>
        /// <exception cref="ArgumentNullException"><paramref name="notification"/> is null.</exception>
        void IHttpResponseModule.Publish(Notification notification)
        {
            if (notification == null)
                throw new ArgumentNullException("notification");

            if (this.wrappers.Count == 0)
                return;

            PublishMission mission = new PublishMission(notification);

            lock (this.locker)
                foreach (ResponseWrapper<PublishMission> wrapper in this.wrappers.Where(w => !w.IsClosed))
                    wrapper.PublishAsync(mission);
        }

        #endregion

        #region Callback

        private void MissionCallback(ResponseWrapper<PublishMission> wrapper, PublishMission mission)
        {
            Notification n = mission.Notify;
            object userState = this.BeforePublish(wrapper.Response.Description, ref n);

            if (n == null)  // The publish action has been cancelled by derived class. 
                return;

            int contentLength = n.ContentLength;
            byte[] headersBuffer = null;

            if (n is IWebNotification)
                headersBuffer = this.CreateHeadersBuffer(Boundary, (n as IWebNotification).Headers);
            else
                headersBuffer = this.CreateHeadersBuffer(Boundary, n.ContentType, contentLength);

            Exception error = null;

            try
            {
                Stream rsp = wrapper.Response.GetResponseStream();

                using (Stream src = n.CreateReadOnlyStream(65536))
                {
                    rsp.Write(headersBuffer, 0, headersBuffer.Length);
                    src.CopyTo(rsp, 65536);
                    rsp.Write(this.boundaryCrlf, 0, this.boundaryCrlfLength);
                }
            }
            catch (Exception e)
            {
                wrapper.Close();
                error = e;
            }

            this.AfterPublish(wrapper.Response.Description, error, userState);
        }

        private void CheckConnectionCallback(object state)
        {
            if (!Monitor.TryEnter(this.locker))
                return;

            int i = 0;   

            while (i < this.wrappers.Count)
            {
                ResponseWrapper<PublishMission> wrapper = this.wrappers[i];

                if (wrapper == null || wrapper.IsClosed)
                {
                    this.wrappers.Remove(wrapper.ID);
                    this.ContextUnregistered(wrapper.Response);

                    continue;
                }
                i++;
            }

            if (this.wrappers.Count == 0 && this.scheduleId.HasValue)
            {
                SchedulePool.Unregister(this.scheduleId.Value);
                this.scheduleId = null;
            }

            Monitor.Exit(this.locker);
        }

        #endregion

        #region Protected Method

        /// <summary>
        /// When implemented in a derived class, executes before a response is registered.
        /// </summary>
        /// <param name="response">A response context that is ready for being registered.</param>
        protected virtual void ContextRegistered(HttpResponseContext response)
        {

        }

        /// <summary>
        /// When implemented in a derived class, executes after a response has unregistered.
        /// </summary>
        /// <param name="response">A response context that has been unregistered.</param>
        protected virtual void ContextUnregistered(HttpResponseContext response)
        {

        }

        /// <summary>
        /// When implemented in a derived class, executes before publishing a notification. 
        /// </summary>
        /// <param name="description">An object that provides informations of current response context.</param>
        /// <param name="notification">A notification which is ready for publishing.</param>
        /// <returns>
        /// The object that is returned as the userState argument of the <see cref="AfterPublish"/> method. 
        /// This is null if no correlation state is used.
        /// </returns>
        protected virtual object BeforePublish(HttpOperationDescription description, ref Notification notification)
        {
            return null;
        }

        /// <summary>
        /// When implemented in a derived class, executes after publishing a notification. 
        /// </summary>
        /// <param name="description">An object that provides informations of current response context.</param>
        /// <param name="error">The error that occurs while publishing a notification.</param>
        /// <param name="userState">The returned value of <see cref="BeforePublish"/> method.</param>
        protected virtual void AfterPublish(HttpOperationDescription description, Exception error, object userState)
        {

        }

        #endregion

        #region Others

        private byte[] CreateHeadersBuffer(string boundary, string contentType, int contentLength)
        {
            string crlfStr = Environment.NewLine;

            string[] headers = new[] 
            {       
                string.Concat(HttpNotification.ContentTypeHeaderField, ": ", contentType), 
                string.Concat(HttpNotification.ContentLengthHeaderField, ": ", contentLength), 
                string.Empty,       
                string.Empty,  
            };

            return this.encoding.GetBytes(string.Join(crlfStr, headers));
        }

        private byte[] CreateHeadersBuffer(string boundary, NameValueCollection headers)
        {
            StringBuilder result = new StringBuilder();
            IEnumerable<string> temps = from key in headers.AllKeys
                                        let value = headers[key]
                                        where !string.IsNullOrEmpty(value)
                                        select string.Concat(key, ": ", value);

            foreach (string temp in temps)
                result.AppendLine(temp);

            result.AppendLine();
            result.AppendLine();

            return this.encoding.GetBytes(result.ToString());
        }

        private void NoContentRespose(ResponseWrapper<PublishMission> wrapper)
        {
            try
            {
                wrapper.Response.StatusCode = HttpStatusCode.NoContent;
            }
            catch (Exception e)
            {
                Debug.WriteLine(e, this.GetType().FullName);
            }
            finally
            {
                wrapper.Close();
                this.ContextUnregistered(wrapper.Response);
            }
        }

        #endregion

        #region Structure

        internal struct PublishMission
        {
            #region Field

            private readonly Notification notification; 

            #endregion

            #region Property

            public Notification Notify
            {
                get { return this.notification; }
            }
             
            #endregion

            #region Constructor

            public PublishMission(Notification notification)
            {
                this.notification = notification;
            }

            #endregion
        }

        #endregion
    }
}